feat(android): Add standalone app start tracing#5342
Conversation
Introduce experimental `enableStandaloneAppStartTracing` option that creates a separate app start transaction instead of attaching app start as a child span of the first activity transaction. This is the happy path only (foreground importance, activity launch, first frame drawn as end time). The standalone transaction shares the same trace ID as the activity transaction but is not bound to the scope. App start measurements and child spans (process init, content providers, application.onCreate) are attached to the standalone transaction instead of the activity transaction. Includes foreground importance check branching to prepare for the non-activity launch path (next PR). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the app starts without launching an activity (service, broadcast receiver, content provider), create a standalone app start transaction with the end time determined by priority: 1. onApplicationPostCreate (Gradle plugin bytecode instrumentation) 2. ApplicationStartInfo timestamps (API 35+) 3. firstIdle - main thread idle handler (pre-API 35 fallback) The non-activity app start transaction stores its trace ID so that if an activity is later launched, the activity transaction reuses the same trace ID to keep both in the same trace. Adds OnNoActivityStartedListener callback from AppStartMetrics to ActivityLifecycleIntegration, triggered by checkCreateTimeOnMain() when no activity was created after Application.onCreate(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…entation When an app is launched via broadcast receiver, service, or content provider (no activity), detect this via Handler.post() and create a standalone app start transaction. Resolves app start end time with priority: Gradle plugin > ApplicationStartInfo (API 35+) > process init time. Also attaches child spans (process init, content providers, Application.onCreate) to standalone transactions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract the "try appStartSpan, fall back to sdkInitTimeSpan" logic used for standalone (non-activity) app start transactions into a new AppStartMetrics.getAppStartTimeSpanDirect() helper, removing the duplicated inline fallback in ActivityLifecycleIntegration and the private helper in PerformanceAndroidEventProcessor. Also cache the API 35+ ApplicationStartInfo on registerLifecycleCallbacks so onAppStartSpansSent no longer re-queries ActivityManager, and simplify the non-activity detection path to always use the main-thread IdleHandler. Regenerates the sentry-android-core API to include method additions missed in prior commits on this branch (standalone-app-start options, trace id accessors, OnNoActivityStartedListener). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires up the TestBroadcastReceiver added earlier so the sample app can trigger a non-activity cold start via `adb shell am broadcast`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tart-tracing # Conflicts: # sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java
…icate emission Two pre-merge fixes for the standalone app-start tracing path introduced on this branch (issue #5046): - AppStartMetrics.checkCreateTimeOnMain() now defaults appStartType to COLD when UNKNOWN with no active activities. On API < 35 (where ApplicationStartInfo is unavailable) non-activity cold starts were stuck at UNKNOWN, which both misclassified the standalone transaction as App Start Warm and caused PerformanceAndroidEventProcessor.attachAppStartSpans to early-return (dropping process.load / application.load / contentprovider.load phase spans). - ActivityLifecycleIntegration.onActivityPreCreated() now skips emitting a second standalone App Start transaction when the non-activity path has already reported the process's app start (detected via the stashed appStartTraceId). Previously a broadcast followed by an activity launch produced two standalone transactions (a spurious App Start Warm in addition to the broadcast's App Start Cold), violating one-per-process semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 319f256 | 322.02 ms | 380.33 ms | 58.30 ms |
| 889ecea | 367.58 ms | 437.52 ms | 69.94 ms |
| 2195398 | 309.39 ms | 354.53 ms | 45.14 ms |
| 72020f8 | 312.32 ms | 370.94 ms | 58.62 ms |
| 62b579c | 318.48 ms | 367.71 ms | 49.24 ms |
| 62b579c | 349.26 ms | 426.26 ms | 77.00 ms |
| dcc6bbf | 382.58 ms | 462.13 ms | 79.54 ms |
| 694d587 | 305.45 ms | 378.38 ms | 72.94 ms |
| 6b019b7 | 343.31 ms | 417.23 ms | 73.91 ms |
| d364ace | 411.72 ms | 430.81 ms | 19.10 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 319f256 | 1.58 MiB | 2.19 MiB | 619.79 KiB |
| 889ecea | 1.58 MiB | 2.11 MiB | 539.75 KiB |
| 2195398 | 0 B | 0 B | 0 B |
| 72020f8 | 1.58 MiB | 2.19 MiB | 620.21 KiB |
| 62b579c | 0 B | 0 B | 0 B |
| 62b579c | 0 B | 0 B | 0 B |
| dcc6bbf | 1.58 MiB | 2.12 MiB | 553.10 KiB |
| 694d587 | 1.58 MiB | 2.19 MiB | 620.06 KiB |
| 6b019b7 | 0 B | 0 B | 0 B |
| d364ace | 1.58 MiB | 2.11 MiB | 539.75 KiB |
Previous results on branch: feat/standalone-app-start-tracing
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5f100fc | 385.33 ms | 474.78 ms | 89.45 ms |
| a419ead | 327.47 ms | 366.96 ms | 39.49 ms |
| ad8b0bb | 318.08 ms | 367.78 ms | 49.70 ms |
| 5f2075f | 329.13 ms | 392.38 ms | 63.25 ms |
| 4d58e3f | 329.78 ms | 405.36 ms | 75.58 ms |
| 8147749 | 302.52 ms | 352.71 ms | 50.19 ms |
| 21e3423 | 315.65 ms | 363.04 ms | 47.39 ms |
| 04a4a5f | 319.55 ms | 374.56 ms | 55.01 ms |
| 3f06ae7 | 312.60 ms | 355.59 ms | 42.99 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5f100fc | 0 B | 0 B | 0 B |
| a419ead | 0 B | 0 B | 0 B |
| ad8b0bb | 0 B | 0 B | 0 B |
| 5f2075f | 0 B | 0 B | 0 B |
| 4d58e3f | 0 B | 0 B | 0 B |
| 8147749 | 0 B | 0 B | 0 B |
| 21e3423 | 0 B | 0 B | 0 B |
| 04a4a5f | 0 B | 0 B | 0 B |
| 3f06ae7 | 0 B | 0 B | 0 B |
Rename the standalone app-start transaction to a single App Start name so cold and warm starts group consistently while preserving the app.start op. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
Co-authored-by: Cursor <cursoragent@cursor.com>
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
Co-authored-by: Cursor <cursoragent@cursor.com>
🚨 Detected changes in high risk code 🚨High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
|
Co-authored-by: Cursor <cursoragent@cursor.com>
…g' into feat/standalone-app-start-tracing
…r is set on API 35+ On API 35+, ApplicationStartInfo resolves appStartType before the standalone app start listener is installed, causing the idle handler condition to be false and skipping the no-activity detection entirely. Register the idle handler from setOnNoActivityStartedListener when the type is already resolved, ensuring onNoActivityStarted() fires for standalone app start tracing on API 35+ devices. Co-authored-by: Cursor <cursoragent@cursor.com>
…p start path The foregroundImportance guard was always true at that point because appStartTime is only set to non-null inside the foregroundImportance branch. Remove the redundant check and the misleading else comment that described an unreachable code path. Co-authored-by: Cursor <cursoragent@cursor.com>
Require the app-start pending flag even when standalone app-start transactions bypass foreground checks. Preserve completed non-activity app-start timings so fallback resolution does not overwrite stopped spans. Co-authored-by: Cursor <cursoragent@cursor.com>
Drop dead AppStartMetrics state that was assigned during lifecycle callback registration but never read. Co-authored-by: Cursor <cursoragent@cursor.com>
| if (appStartType == AppStartType.UNKNOWN) { | ||
| appStartType = AppStartType.COLD; | ||
| } |
There was a problem hiding this comment.
so basically pre-API 35 we don't really have a way to reliably tell in non-activity starts afaict - are we ok in classifying these as cold starts?
…ForStandalone Co-authored-by: Cursor <cursoragent@cursor.com>
…pp start spans Co-authored-by: Cursor <cursoragent@cursor.com>
…ead visibility The field is written by setOnNoActivityStartedListener (called during Sentry.init(), potentially on a background thread) and read on the main thread in handleNoActivityStartIfNeededOnMain. Without volatile, the JMM permits the main thread to see a stale null, silently skipping the listener and preventing standalone app-start transaction creation. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b871644. Configure here.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in Android feature to emit standalone app start transactions (op=app.start) instead of nesting app-start spans under the first ui.load transaction, including support for non-Activity process starts (e.g., broadcast/service) and traceId reuse when an Activity starts later.
Changes:
- Introduces
enableStandaloneAppStartTracing(option + manifest meta-data key) and wires it intoActivityLifecycleIntegration. - Updates app-start metric/span attachment to support standalone
app.starttransactions and non-Activity starts (including API 35+ApplicationStartInfoend-time resolution). - Adds/updates tests, API dumps, and sample app wiring for the new behavior.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| sentry/src/test/java/io/sentry/TransactionContextTest.kt | Adds coverage for new traceId-reuse TransactionContext constructor behavior. |
| sentry/src/main/java/io/sentry/TransactionContext.java | Adds internal constructor to create a TransactionContext with a provided traceId. |
| sentry/api/sentry.api | Updates public API surface dump for the new TransactionContext constructor. |
| sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/TestBroadcastReceiver.java | Adds a manifest receiver used for manual non-Activity cold-start testing. |
| sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml | Registers the test receiver and enables standalone app-start tracing via manifest meta-data. |
| sentry-android-core/src/test/java/io/sentry/android/core/SentryShadowProcess.kt | Extends Robolectric shadow to support Process.getStartElapsedRealtime(). |
| sentry-android-core/src/test/java/io/sentry/android/core/SentryShadowActivityManager.kt | Extends Robolectric shadow to support importance/memory-state queries. |
| sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt | Verifies standalone app-start tracing is disabled by default. |
| sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt | Adds standalone app-start transaction processing tests (measurements + span parenting). |
| sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTestApi35.kt | Adds API 35 tests for ApplicationStartInfo type/end-time resolution and listener behavior. |
| sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt | Adds/updates tests for non-Activity start classification/end-time resolution + helper APIs. |
| sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt | Adds tests for reading the manifest meta-data flag into options. |
| sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt | Adds extensive coverage for standalone app-start creation, finishing, and traceId reuse. |
| sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java | Adds enableStandaloneAppStartTracing option with detailed behavior docs. |
| sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java | Attaches app-start measurements/spans for standalone app.start transactions and adjusts TTID/TTFD contributing flags. |
| sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java | Adds non-Activity listener + traceId stash + API 35 timestamp-based end-time resolution. |
| sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java | Reads io.sentry.standalone-app-start-tracing.enable into options. |
| sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java | Implements standalone app-start transaction creation (activity + non-activity paths) and traceId reuse. |
| sentry-android-core/api/sentry-android-core.api | Updates Android-core API surface dump for the newly exposed methods/option. |
| CHANGELOG.md | Documents the new opt-in standalone app-start tracing feature and configuration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… start path onNoActivityStarted() did not clear the appStartSamplingDecision, which could leak to the first ui.load transaction when an activity eventually starts after a non-activity process launch. Co-authored-by: Cursor <cursoragent@cursor.com>
cce4bf4 to
e681be9
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cdc6178. Configure here.
|
|
||
| if (noActivityStartedListener != null) { | ||
| noActivityStartedListener.onNoActivityStarted(); | ||
| } |
There was a problem hiding this comment.
Volatile field TOCTOU race may cause NPE
Low Severity
The noActivityStartedListener field is volatile but the null-check-then-invoke in handleNoActivityStartIfNeededOnMain is not atomic. If close() on ActivityLifecycleIntegration runs on a non-main thread and sets the listener to null between the null check and the method call, a NullPointerException results. Saving the volatile read to a local variable before the null check would eliminate this race.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit cdc6178. Configure here.


📜 Description
Adds standalone Android app-start transaction support behind the opt-in
enableStandaloneAppStartTracingoption and theio.sentry.standalone-app-start-tracing.enablemanifest key.This PR also covers the non-activity startup path so cold starts triggered by broadcasts or foreground services can emit an
App Start Cold/Warmtransaction without waiting for an activity. The legacy flag-off behavior is preserved: activity app-start data remains nested under theui.loadtransaction.Additionally this PR adds a new attribute:
app.vitals.start.screen💡 Motivation and Context
Resolves: #5046
Standalone app-start traces make app-start performance visible independently from activity load transactions, and they let Android report app starts where no activity is launched. The implementation keeps the default behavior unchanged and adds tests for the new option, manifest metadata, transaction context trace reuse, app-start metrics, event processing, and activity lifecycle behavior.
App-start creation paths
This PR preserves the existing flag-off behavior and adds a separate standalone app-start branch when
enableStandaloneAppStartTracingis enabled.ActivityLifecycleIntegrationstarts a standaloneApp Starttransaction and the normal activityui.loadtransaction on the same trace.app.startfor app-start phases, andui.loadfor TTID/TTFD. Theui.loadtransaction does not contain anapp.start.cold/warmchild.ui.loadtransaction with nestedapp.start.cold/warm, plus app-start phase spans under that child.Application.onCreatestart/end throughAppStartMetrics.onApplicationCreate/PostCreate. When no activity starts,ActivityLifecycleIntegrationemits from that completed app-start span.App Starttransaction and noui.load; includesprocess.loadandapplication.loadphase spans.AppStartMetricsreadsApplicationStartInfofromActivityManagerto determine start type and use the platformAPPLICATION_ONCREATEtimestamp as the app-start end time.App Starttransaction and noui.load; includesprocess.load, but notapplication.loadbecause the SDK does not have plugin instrumentation for that phase.ApplicationStartInfois unavailable, soAppStartMetricsdefaults the no-activity process start to cold and uses the class-loaded timestamp fallback for the app-start end time.App Starttransaction and noui.load; includesprocess.loadonly and remains classified as cold.App Starttransaction stores its trace ID for a later activity.ui.loadreuses the stored trace ID and does not create a second standalone app-start transaction.💚 How did you test it?
./gradlew spotlessApply apiDumpscripts/test-standalone-app-start.sh, summarized below.End-to-end Sentry verification
App Startemits alongside siblingMainActivity ui.load; shared trace ID;ui.loadhas noapp.start.*child.app.start(app_start_cold=1156ms) +activity.loadx2 +process.load+application.load; siblingui.load(time_to_initial_display=1156ms)52154e91...ui.loadwithapp.start.coldnested inside.ui.load(time_to_initial_display=1536ms) -> nestedapp.start.cold(app_start_cold=1537ms) ->process.load+activity.loadx2d3f0e049...app.start(app_start_cold=1408ms) +process.load+application.load; noui.load81ae80f6...ApplicationStartInfo.app.start(app_start_cold=525ms) +process.loadonly; noui.load0dcd120e...CLASS_LOADED_UPTIME_MS.app.start(app_start_cold=614ms) +process.loadonly; correctly classified Coldac8a9189...app.start(app_start_cold=4605ms) +process.load+application.load; noui.load905262ce...app.start(app_start_cold=1751ms) + later siblingui.load(time_to_initial_display=4563ms); no duplicate standalone5da4a88f...📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps